package org.gap.wizards.utils;

import java.awt.Dimension;
import java.awt.Point;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;

import org.gap.jseed.SeedContainer;

public class FlatFilePersistence implements Persistence {
	private Map<String, Object> values = new HashMap<String, Object>(); 
	public static final File HOME_DIRECTORY;
	
	private static final String NUMBER_OF = "number.of.";
	private static final String CLASS = "class";
	
	private Stack<String> sections;
	private File file;
	private BufferedWriter writer;
	private SeedContainer container;
	private final boolean readOnly;
	private final String[] path;

	static {
		HOME_DIRECTORY = new File(System.getProperty("user.home"));
	}
	
	public FlatFilePersistence(boolean readOnly, String name, String...path) throws IOException {
		this.readOnly = readOnly;
		this.path = path;
		sections = new Stack<String>();
		File directory = ensurePath(HOME_DIRECTORY, path);
		file = new File(directory, getSectionNamedKey(name));
		if (file.exists()) {
			readFile();
		} else if (!readOnly){
			file.createNewFile();
		}
		if (readOnly) {
			writer = new BufferedWriter(new StringWriter());
		} else {
			writer = new BufferedWriter(new FileWriter(file));
		}
	}

	private File ensurePath(File currentDirectory, String... path) {
		for (String each : path) {
			currentDirectory = new File(currentDirectory, getSectionNamedKey(each));
			if (!currentDirectory.exists()) {
				currentDirectory.mkdir();
			}
		}
		return currentDirectory;
	}
	
	private void readFile() throws IOException {
		BufferedReader reader = new BufferedReader(new FileReader(file));
		String line = null;
		while ((line = reader.readLine()) != null) {
			if (line.contains("=")) {
				String[] keyValuePair = line.split("=");
				values.put(getSectionNamedKey(keyValuePair[0].trim()), keyValuePair[1]);
			} else if (line.endsWith("{")) {
				sections.push(line.substring(0, line.length() - 2));
			} else if (line.endsWith("}")) {
				sections.pop();
			}
		}
	}

	@Override
	public void set(String key, String value) {
		tryWriteString(key, value);
	}
	
	@Override
	public void set(String key, boolean value) {
		tryWriteString(key, Boolean.toString(value));
	}
	
	@Override
	public void set(String key, int value) {
		tryWriteString(key, Integer.toString(value));
	}
	
	@Override
	public void set(String key, Point point) {
		tryWriteString(key, pointToString(point));
	}

	private String pointToString(Point point) {
		return "(" + point.x + "," + point.y + ")";
	}
	
	@Override
	public void set(String key, Dimension point) {
		tryWriteString(key, dimensionToString(point));
	}

	private String dimensionToString(Dimension dimensions) {
		return "(" + dimensions.width + "," + dimensions.height + ")";
	}
	
	private void tryWriteString(String key, String value) {
		try {
			writer.append(getTabbing() + key + "=" + value);
			writer.newLine();
		} catch (IOException e) {
			e.printStackTrace();
			throw new FlatFilePersistenceException("Unable to write key:value pair [" + getSectionNamedKey(key) + "=" + getSectionNamedKey(value) + "]", e);
		}

		values.put(getSectionNamedKey(key), value);
	}

	private String getTabbing() {
		String tabs = "";
		for (int i = 0; i < sections.size(); i++) {
			tabs += "\t";
		}
		return tabs;
	}

	private String getSectionNamedKey(String key) {
		String name = "";
		for (String each : sections) {
			name += each.trim() + ".";
		}
		return name + key;
	}
	
	@Override
	public void set(String key, Class<?> klass) {
		tryWriteString(key, klass.getName().split("-")[0]);
	}
	
	@Override
	public void set(String key, Collection<String> keySet) {
		set(key, keySet.toArray(new String[keySet.size()]));
	}
	
	@Override
	public void set(String key, String[] strings) {
		tryWriteString(key, arrayValuesAsString(strings, new ToString<String>() {
			@Override
			public String toString(String point) {
				return point;
			}
		}));
	}
	
	@Override
	public void set(String key, Point[] points) {
		tryWriteString(key, arrayValuesAsString(points, new ToString<Point>() {
			@Override
			public String toString(Point point) {
				return pointToString(point);
			}
		}));
	}
	
	private <T> String arrayValuesAsString(T[] array, ToString<T> convert) {
		String value = "";
		for (T each : array) {
			if (!value.isEmpty()) {
				value += ", ";
			}
			value += convert.toString(each);
		}
		return "[" + value + "]";
	}
	
	@Override
	public void set(String key, Map<?, ?> values) {
		tryWriteString(key, mapAsStringValues(values));
	}
	
	@Override
	public void set(String key, List<? extends Persistable> toPersist) {
		set(numberOf(key), toPersist.size());
		int number = 0;
		for (Persistable each : toPersist) {
			beginSection(key + (number++));
			set(CLASS, each.getClass().getName());
			each.persist(this);
			endSection();
		}		
	}
	
	@Override
	public <T extends Persistable> List<T> getList(String key) {
		int numberOfCreatures = getInt(numberOf(key));
		ArrayList<T> result = new ArrayList<T>();
		for (int i = 0; i < numberOfCreatures; i++) {
			beginSection(key + (i));
			@SuppressWarnings("unchecked")
			T aValue = (T) getClass(CLASS);
			aValue.load(this);
			result.add(aValue);
			endSection();
		}
		return result;
	}
	
	private String numberOf(String key) {
		return NUMBER_OF + key + "s";
	}
	
	@Override
	public void set(String key, Enum<?> value) {
		set(key + "." + CLASS, value.getClass());
		set(key, value.name());
	}
	
	@Override
	public <T extends Enum<T>> T getEnum(String key, Class<T> klass) {
		String name = getString(key);
		return Enum.valueOf(klass, name);
	}
	
	private String mapAsStringValues(Map<?, ?> values) {
		String result = "";
		for (Entry<?, ?> each : values.entrySet()) {
			if (!result.isEmpty()) {
				result += ", ";
			}
			result += "[" + withClass(each.getKey()) + ":" + withClass(each.getValue()) + "]";
		}
		return "[" + result + "]";
	}

	private String withClass(Object value) {
		if (value instanceof String || value instanceof Number) {
			return value.getClass().getName() + ":" + value.toString();
		} else if (value instanceof Point) {
			return value.getClass().getName() + ":" + pointToString((Point)value);
		} else if (value instanceof Dimension) {
			return value.getClass().getName() + ":" + dimensionToString((Dimension)value);
		}
		throw new FlatFilePersistenceException("Unknown type given [" + value.getClass().getName() + "]");
	}

	@Override
	public String getString(String key) {
		return (String)values.get(getSectionNamedKey(key));
	}
	
	@Override
	public boolean getBoolean(String key) {
		return Boolean.parseBoolean((String)values.get(getSectionNamedKey(key)));
	}
	
	@Override
	public int getInt(String key) {
		return Integer.parseInt((String)values.get(getSectionNamedKey(key)));
	}

	@Override
	public Point getPoint(String key) {
		String[] split = ((String)values.get(getSectionNamedKey(key))).split(",");
		int x = getIntFromString(split[0].substring(1));
		int y = getIntFromString(split[1].substring(0, split[1].length() - 1));
		return new Point(x, y);
	}

	private int getIntFromString(String value) {
		return Integer.parseInt(value.trim());
	}

	@Override
	public Dimension getDimension(String key) {
		String[] split = ((String)values.get(getSectionNamedKey(key))).split(",");
		int x = getIntFromString(split[0].substring(1));
		int y = getIntFromString(split[1].substring(0, split[1].length() - 1));
		return new Dimension(x, y);
	}
	
	@Override
	public Object getClass(String key) {
		String className = (String)values.get(getSectionNamedKey(key));
		try {
			Class<?> klass = Class.forName(className);
			try {
				klass.getConstructor(); // Checks if default constructor exists.
				return klass.newInstance();
			} catch (NoSuchMethodException e) {
			} catch (SecurityException e) {
			}
			return container.create(klass);
		} catch (InstantiationException e) {
			throw new FlatFilePersistenceException("Ensure default constructor, ensure the same code base is used [" + className + "]", e);
		} catch (IllegalAccessException e) {
			throw new FlatFilePersistenceException("Access to class has changed, ensure the same code base is used [" + className + "]", e);
		} catch (ClassNotFoundException e) {
			throw new FlatFilePersistenceException("Class not found, ensure the same code base is used [" + className + "]", e);
		}
	}

	@Override
	public String[] getStrings(String key) {
		String[] split = getStringAndStripBrackets(key).split(", ");
		if (split.length == 1 && split[0].isEmpty()) {
			return new String[] {};
		}
		String[] result = new String[split.length];
		for (int i = 0; i < split.length; i++) {
			result[i] = split[i].trim();
		}
		return result;
	}
	
	@Override
	public Point[] getPoints(String key) {
		String[] xAndYCoordinates = getStringAndStripBrackets(key).split(",");
		Point[] points = new Point[xAndYCoordinates.length / 2];
		if (points.length == 0) {
			return points;
		}
		for (int i = 0; i < xAndYCoordinates.length; i += 2) {
			String xData = xAndYCoordinates[i].trim();
			int x =  getIntFromString(xData.substring(1));

			String yData = xAndYCoordinates[i + 1].trim();
			int y = getIntFromString(yData.substring(0, yData.length() - 1));
			
			points[i / 2] = new Point(x, y);
		}
		return points;
	}

	private String getStringAndStripBrackets(String key) {
		String result = (String)values.get(getSectionNamedKey(key));
		return stripBrackets(result);
	}

	private String stripBrackets(String result) {
		return result.substring(1, result.length() - 1);
	}
	
	@Override
	public Map<?, ?> getMap(String key) {
		return asMap(getStringAndStripBrackets(key));
	}
	
	@SuppressWarnings("unchecked")
	private Map<?, ?> asMap(String line) {
		@SuppressWarnings("rawtypes")
		Map result = new HashMap();
		if (line.isEmpty()) {
			return result;
		}
		for (String each : line.split(", ")) {
			String[] classDataPair = stripBrackets(each).split(":");
			Object key = getObjectFromString(classDataPair[0], classDataPair[1]);
			Object value = getObjectFromString(classDataPair[2], classDataPair[3]);
			result.put(key, value);
		}
		
		return result;
	}

	private Object getObjectFromString(String klassName, String value) {
		if (String.class.getName().equals(klassName)) {
			return value;
		} else if (Integer.class.getName().equals(klassName)) {
			return Integer.parseInt(value);
		} else if (Point.class.getName().equals(klassName)) {
			String[] split = value.split(",");
			int x = getIntFromString(split[0].substring(1));
			int y = getIntFromString(split[1].substring(0, split[1].length() - 1));
			return new Point(x, y);
		}
		throw new FlatFilePersistenceException("Unexpected key/value in map [" + klassName + ", " + value + "]");
	}

	@Override
	public void beginSection(String name) {
		try {
			writer.append(getTabbing() + name + " {");
			writer.newLine();
		} catch (IOException e) {
			new FlatFilePersistenceException("Unable to begin write of section [" + name + "]", e);
		}
		sections.push(name);
	}
	
	@Override
	public void endSection() {
		String name = sections.pop();
		try {
			writer.append(getTabbing() + "}");
			writer.newLine();
		} catch (IOException e) {
			new FlatFilePersistenceException("Unable to end write of section [" + name + "]", e);
		}
	}
	
	@Override
	public void save() {
		try {
			writer.flush();
			writer.close();
		} catch (IOException e) {
			new FlatFilePersistenceException("Failed to flush and close the file [" + file.getName() + "]", e);
		}
	}
	
	@Override
	public void setSeedContainer(SeedContainer container) {
		this.container = container;
	}
	
	/**
	 * Simple factory method to allow creation of new storages local to this storage.
	 */
	@Override
	public Persistence newPersister(String persistenceName){
		try {
			Persistence persistence = new FlatFilePersistence(readOnly, persistenceName, path);
			persistence.setSeedContainer(container);
			return persistence;
		} catch (IOException e) {
			throw new FlatFilePersistenceException("Unable to create new Persistence storage: " + persistenceName, e);
		}
	}
	
	public interface ToString<T> {
		String toString(T persistable);
	}
	
	public class FlatFilePersistenceException extends RuntimeException {
		private static final long serialVersionUID = 7741731762467911090L;

		public FlatFilePersistenceException(String message, Exception e) {
			super(message, e);
		}

		public FlatFilePersistenceException(String message) {
			super(message);
		}
	}
}
